//////////////////////////////////////////////
// main.cpp
//
//////////////////////////////////////////////

/// Includes ---------------------------------

// nkLog
#include <NilkinsLog/Loggers/ConsoleLogger.h>

// nkGraphics
#include <NilkinsGraphics/Cameras/Camera.h>
#include <NilkinsGraphics/Cameras/CameraManager.h>

#include <NilkinsGraphics/Encoders/Obj/ObjEncoder.h>

#include <NilkinsGraphics/Entities/Entity.h>

#include <NilkinsGraphics/Graph/Node.h>
#include <NilkinsGraphics/Graph/NodeManager.h>

#include <NilkinsGraphics/Log/LogManager.h>

#include <NilkinsGraphics/Meshes/Utils/MeshUtils.h>

#include <NilkinsGraphics/Meshes/Mesh.h>
#include <NilkinsGraphics/Meshes/MeshManager.h>

#include <NilkinsGraphics/RenderContexts/RenderContextDescriptor.h>
#include <NilkinsGraphics/RenderContexts/RenderContextManager.h>

#include <NilkinsGraphics/RenderQueues/RenderQueue.h>
#include <NilkinsGraphics/RenderQueues/RenderQueueManager.h>

#include <NilkinsGraphics/System.h>

// nkResources
#include <NilkinsResources/ResourceManager.h>

// nkWinUi
#include <NilkinsWinUi/System.h>

// Standards
#include <chrono>

#include <memory>

/// Internals --------------------------------

void prepareMeshFromFile ()
{
	// Initialize mesh to use later
	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	// Load data from file
	nkMemory::String absPath = nkResources::ResourceManager::getInstance()->getAbsoluteFromWorkingDir("sphere.obj") ;
	nkMemory::Buffer objData = nkResources::ResourceManager::getInstance()->loadFileIntoMemory(absPath) ;

	// We change some settings to alter the way the mesh is imported
	nkGraphics::ObjDecodeOptions objOptions ;
	objOptions._invertUvY = true ;
	objOptions._invertWindingOrder = true ;
	nkGraphics::DecodedData objDecoded = nkGraphics::ObjEncoder::decode(objData, objOptions) ;

	// Fill mesh
	nkGraphics::MeshFillOptions fillOptions ;
	fillOptions._autoLoad = true ;
	fillOptions._dataFillType = nkGraphics::DATA_FILL_TYPE::FORWARD ;
	nkGraphics::MeshUtils::fillMeshFromDecodedData(objDecoded._meshData[0], mesh, fillOptions) ;
}

void addMeshToRenderQueue ()
{
	// Add the mesh to the render queue to visualize it
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;

	nkGraphics::Mesh* mesh = nkGraphics::MeshManager::getInstance()->createOrRetrieve("Mesh") ;

	nkGraphics::Entity* ent = rq->addEntity() ;
	ent->setRenderInfo(nkGraphics::EntityRenderInfo(mesh, nullptr)) ;
}

void addEntityToGraph ()
{
	// Move the entity around using the node graph
	nkGraphics::RenderQueue* rq = nkGraphics::RenderQueueManager::getInstance()->get(nkGraphics::RenderQueueManager::DEFAULT_RENDER_QUEUE) ;
	nkGraphics::Entity* ent = rq->getEntity(0) ;

	nkGraphics::Node* node = nkGraphics::NodeManager::getInstance()->createOrRetrieve("node") ;
	node->setPositionAbsolute(nkMaths::Vector(0.f, 0.f, 10.f)) ;

	ent->setParentNode(node) ;
}

/// Function ---------------------------------

int main ()
{
	// Prepare for logging
	std::unique_ptr<nkLog::Logger> logger = std::make_unique<nkLog::ConsoleLogger>() ;
	nkGraphics::LogManager::getInstance()->setReceiver(logger.get()) ;

	// For easiness
	nkResources::ResourceManager::getInstance()->setWorkingPath("Data") ;

	// Initialize and create context with window
	if (!nkGraphics::System::getInstance()->initialize())
		return -1 ;

	nkGraphics::RenderContext* context = nkGraphics::RenderContextManager::getInstance()->createRenderContext(nkGraphics::RenderContextDescriptor(800, 600, false, true)) ;

	// Prepare the mesh we will show
	prepareMeshFromFile() ;
	addMeshToRenderQueue() ;
	addEntityToGraph() ;

	nkGraphics::Camera* camera = nkGraphics::CameraManager::getInstance()->getDefaultCamera() ;

	// Two loops are demonstrated in the tutorial
	// The first one, disabled by default, only reproduces what the run function does
	// Second one will make the camera rotate around the sphere in between frame calls
	if (false)
	{
		// Retrieve some systems to address them directly
		nkGraphics::System* nkGraphicsSystem = nkGraphics::System::getInstance() ;
		nkWinUi::System* nkWinUiSystem = nkGraphicsSystem->getUiSystem() ;

		// Check whether the system tells us to stop or not
		while (nkGraphicsSystem->getHasRunToContinue())
		{
			// If not, then generate a new frame
			if (!nkGraphicsSystem->frame(context))
				break ;

			// Make the UI system tick for event processing, required for our window to work properly
			nkWinUiSystem->tick() ;
		}
	}
	else
	{
		// Retrieve some systems to address them directly
		nkGraphics::System* nkGraphicsSystem = nkGraphics::System::getInstance() ;
		nkWinUi::System* nkWinUiSystem = nkGraphicsSystem->getUiSystem() ;

		// Prepare some values for our loop
		const float loopTimeMs = 5 * 1000.f ;
		std::chrono::system_clock::time_point firstNow = std::chrono::system_clock::now() ;

		const nkMaths::Vector sphereCenter = nkMaths::Vector(0.f, 0.f, 10.f) ;

		while (nkGraphicsSystem->getHasRunToContinue())
		{
			// Generate an image by framing
			if (!nkGraphicsSystem->frame(context))
				break ;

			// And move a bit the camera around
			// The camera will rotate around the sphere
			std::chrono::system_clock::time_point now = std::chrono::system_clock::now() ;
			float currentTimeMs = std::chrono::duration_cast<std::chrono::microseconds>(now - firstNow).count() / 1000.f ;
			
			float currentLoopT = (std::fmod(currentTimeMs, loopTimeMs) / loopTimeMs) * 2.f - 1.f ;
			currentLoopT *= 3.14 ;
			
			nkMaths::Vector loopPos (std::cos(currentLoopT) * 10.f, 0.f, std::sin(currentLoopT) * 10.f) ;
			loopPos += sphereCenter ;

			camera->setPositionAbsolute(loopPos) ;
			camera->lookAt(sphereCenter, nkMaths::Vector(0.f, 1.f, 0.f)) ;

			// Make the UI system tick for event processing, required for our window to work properly
			nkWinUiSystem->tick() ;
		}
	}

	// Clean exit
	nkGraphics::System::getInstance()->kill() ;
}